From 538d933baef56d7ee76f78617b553d63713efa24 Mon Sep 17 00:00:00 2001 From: Biswa Kalyan Bhuyan Date: Sun, 27 Apr 2025 23:02:42 +0530 Subject: finance: feat: added the goal page with some improvements of ui --- frontend/src/app/(main)/goals/[id]/page.tsx | 290 ++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 frontend/src/app/(main)/goals/[id]/page.tsx (limited to 'frontend/src/app/(main)/goals/[id]') diff --git a/frontend/src/app/(main)/goals/[id]/page.tsx b/frontend/src/app/(main)/goals/[id]/page.tsx new file mode 100644 index 0000000..3428ca4 --- /dev/null +++ b/frontend/src/app/(main)/goals/[id]/page.tsx @@ -0,0 +1,290 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; +import { Edit, ArrowLeft, Loader2, RefreshCw } from "lucide-react"; +import { useToast } from "@/components/ui/use-toast"; +import { formatCurrency } from "@/lib/utils"; +import { api } from "@/lib/api"; +import { GoalProgress } from "../components/goals-list"; + +export default function GoalDetailPage({ params }: { params: { id: string } }) { + const id = params.id; + const goalId = parseInt(id); + + const [goal, setGoal] = useState(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const router = useRouter(); + const { toast } = useToast(); + + const fetchGoalDetails = useCallback(async () => { + try { + console.log(`Fetching goal details for ID: ${goalId}`); + setLoading(true); + + // Add cache-busting parameter + const response = await api.get(`/goals/${goalId}/progress?cache=${new Date().getTime()}`); + console.log("Goal details received:", response.data); + + // Validate and normalize data + const data = response.data; + if (data && data.goal) { + const sanitizedData = { + ...data, + goal: { + ...data.goal, + targetAmount: Number(data.goal.targetAmount) || 0, + currentAmount: Number(data.goal.currentAmount) || 0, + createdAt: data.goal.createdAt || new Date().toISOString(), + }, + percentComplete: Number(data.percentComplete) || 0, + amountRemaining: Number(data.amountRemaining) || 0, + daysRemaining: Number(data.daysRemaining) || 0, + requiredPerDay: Number(data.requiredPerDay) || 0, + requiredPerMonth: Number(data.requiredPerMonth) || 0, + }; + console.log("Processed goal data:", sanitizedData); + setGoal(sanitizedData); + } else { + console.error("Invalid goal data format:", data); + throw new Error("Invalid goal data received"); + } + } catch (error) { + console.error("Error fetching goal details:", error); + toast({ + title: "Error", + description: "Failed to fetch goal details. Please try again.", + variant: "destructive", + }); + router.push("/goals"); + } finally { + setLoading(false); + } + }, [goalId, toast, router]); + + // Fetch goal details when component mounts + useEffect(() => { + if (!id) { + toast({ + title: "Error", + description: "Goal ID is missing. Please try again.", + variant: "destructive", + }); + router.push("/goals"); + return; + } + + fetchGoalDetails(); + }, [id, fetchGoalDetails, router, toast]); + + const recalculateProgress = async () => { + if (isNaN(goalId)) { + toast({ + title: "Error", + description: "Invalid goal ID", + variant: "destructive", + }); + return; + } + + try { + setRefreshing(true); + await api.post(`/goals/${goalId}/recalculate`); + toast({ + title: "Progress recalculated", + description: "Your goal progress has been recalculated based on transactions.", + }); + fetchGoalDetails(); + } catch (error) { + toast({ + title: "Error", + description: "Failed to recalculate goal progress. Please try again.", + variant: "destructive", + }); + console.error("Error recalculating goal progress:", error); + } finally { + setRefreshing(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (!goal) { + return ( +
+

Goal not found or access denied.

+ + + +
+ ); + } + + const { goal: goalData, percentComplete, amountRemaining, daysRemaining, requiredPerDay, requiredPerMonth, onTrack } = goal; + const isCompleted = goalData.status === "Achieved"; + + return ( +
+
+ + + +
+ +
+
+

{goalData.name}

+

+ {isCompleted + ? "Goal has been achieved 🎉" + : onTrack + ? "Progress is on track" + : "Progress is behind schedule"} +

+
+
+ + + + +
+
+ +
+ + +
+ Goal Progress + + {isCompleted ? "Achieved" : onTrack ? "On Track" : "Behind"} + +
+
+ +
+
+ Completion + {Math.round(percentComplete)}% +
+ +
+ +
+
+
+

Target Amount

+

{formatCurrency(goalData.targetAmount)}

+
+
+

Current Amount

+

{formatCurrency(goalData.currentAmount)}

+
+
+

Remaining

+

{formatCurrency(amountRemaining)}

+
+
+ +
+ {goalData.targetDate && ( +
+

Target Date

+

{new Date(goalData.targetDate).toLocaleDateString()}

+
+ )} + {daysRemaining > 0 && ( + <> +
+

Days Remaining

+

{daysRemaining} days

+
+
+

Required Per Day

+

{formatCurrency(requiredPerDay)}

+
+
+

Required Per Month

+

{formatCurrency(requiredPerMonth)}

+
+ + )} +
+
+
+
+ + + + Goal Details + + +
+
+

Goal Name

+

{goalData.name}

+
+
+

Purpose

+

{goalData.name}

+
+
+

Status

+

{goalData.status}

+
+
+

Created

+

{new Date(goalData.createdAt).toLocaleDateString()}

+
+ {isCompleted ? ( +
+
+

🎉 Goal achieved!

+

+ Congratulations on achieving your financial goal. +

+
+
+ ) : ( +
+ + + +
+ )} +
+
+
+
+
+ ); +} \ No newline at end of file -- cgit v1.2.3-59-g8ed1b